<!--
Copyright (c) 2019 The Khronos Group Inc.
Use of this source code is governed by an MIT-style license that can be
found in the LICENSE.txt file.
-->

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WebGL Transform Feedback Conformance Tests</title>
<link rel="stylesheet" href="../../resources/js-test-style.css"/>
<script src="../../js/js-test-pre.js"></script>
<script src="../../js/webgl-test-utils.js"></script>
</head>
<body>
<div id="description"></div>
<canvas id="canvas" style="width: 50px; height: 50px;"> </canvas>
<div id="console"></div>
<script id="vshader" type="x-shader/x-vertex">#version 300 es

in vec4 in_data;
out vec4 out_add;
out vec4 out_mul;
void main(void) {
    out_add = in_data + vec4(2.0, 3.0, 4.0, 5.0);
    out_mul = in_data * vec4(2.0, 3.0, 4.0, 5.0);
}
</script>
<script id="fshader" type="x-shader/x-fragment">#version 300 es
precision mediump float;
out vec4 out_color;
void main(void) {
    out_color = vec4(1.0, 1.0, 1.0, 1.0);
}
</script>
<script>
"use strict";
description("This test verifies the functionality of the Transform Feedback objects.");

debug("");

var wtu = WebGLTestUtils;
var canvas = document.getElementById("canvas");
var gl = wtu.create3DContext(canvas, null, 2);
var tf = null;
var tf1 = null;
var buf = null;
let out_add_buffer = null;
var program = null;
var activeInfo = null;
var query = null;
var numberOfQueryCompletionAttempts = 0;

if (!gl) {
    testFailed("WebGL context does not exist");
} else {
    testPassed("WebGL context exists");

    runBindingTest();
    runTFBufferBindingTest();
    runObjectTest();
    runGetBufferSubDataTest();
    runUnboundDeleteTest();
    runBoundDeleteTest();
    runOneOutputFeedbackTest();
    runUnchangedBufferBindingsTest();
    runNoOutputsTest();
    // Must be the last test, since it's asynchronous and calls finishTest().
    runTwoOutputFeedbackTest();
}

function runBindingTest() {
    debug("");
    debug("Testing binding enum");

    shouldBe("gl.TRANSFORM_FEEDBACK_BINDING", "0x8E25");

    gl.getParameter(gl.TRANSFORM_FEEDBACK_BINDING);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "TRANSFORM_FEEDBACK_BINDING query should succeed");

    // Default value is null
    shouldBe("gl.getParameter(gl.TRANSFORM_FEEDBACK_BINDING)", "null");

    debug("Testing binding a Transform Feedback object");
    tf = gl.createTransformFeedback();
    tf1 = gl.createTransformFeedback();
    shouldBeNull("gl.getParameter(gl.TRANSFORM_FEEDBACK_BINDING)");
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
    shouldBe("gl.getParameter(gl.TRANSFORM_FEEDBACK_BINDING)", "tf");
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf1);
    shouldBe("gl.getParameter(gl.TRANSFORM_FEEDBACK_BINDING)", "tf1");
    gl.deleteTransformFeedback(tf);
    gl.deleteTransformFeedback(tf1);
    shouldBeNull("gl.getParameter(gl.TRANSFORM_FEEDBACK_BINDING)");
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf1);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "binding a deleted Transform Feedback object");
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
    shouldBeNull("gl.getParameter(gl.TRANSFORM_FEEDBACK_BINDING)");
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "there should be no errors");
}

function runTFBufferBindingTest() {
    debug("");
    debug("Testing binding and unbinding transform feedback objects and buffers");

    buf = gl.createBuffer();
    gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, buf);
    gl.bufferData(gl.TRANSFORM_FEEDBACK_BUFFER, 16, gl.STATIC_DRAW);
    gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, null);

    tf = gl.createTransformFeedback();
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
    gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buf);
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);

    // gl.TRANSFORM_FEEDBACK_BUFFER is part of Transform Feedback objects'
    // state. See OpenGL ES 3.0.5 Section 6.24.
    //
    // Since the TRANSFORM_FEEDBACK was just unbound, there should be nothing
    // bound.
    shouldBeNull('gl.getIndexedParameter(gl.TRANSFORM_FEEDBACK_BUFFER_BINDING, 0)');

    // Binding the buffer to the ARRAY_BUFFER binding point should succeed.
    gl.bindBuffer(gl.ARRAY_BUFFER, buf);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "binding buffer to ARRAY_BUFFER");

    gl.bindBuffer(gl.ARRAY_BUFFER, null);
    gl.deleteBuffer(buf);
    buf = null;
    gl.deleteTransformFeedback(tf);
    tf = null;
}

function runObjectTest() {
    debug("");
    debug("Testing object creation");

    tf = gl.createTransformFeedback();
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "createTransformFeedback should not set an error");
    shouldBeNonNull("tf");

    // Expect false if never bound
    shouldBeFalse("gl.isTransformFeedback(tf)");
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
    shouldBeTrue("gl.isTransformFeedback(tf)");
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
    shouldBeTrue("gl.isTransformFeedback(tf)");
    gl.deleteTransformFeedback(tf);
    shouldBeFalse("gl.isTransformFeedback(tf)");

    shouldBeFalse("gl.isTransformFeedback(null)");

    tf = null;
}

function runOneOutputFeedbackTest() {
    debug("");
    debug("Testing one-output transform feedback");

    // Build the input and output buffers
    var in_data = [
        1.0, 2.0, 3.0, 4.0,
        2.0, 4.0, 8.0, 16.0,
        0.75, 0.5, 0.25, 0.0
    ];

    var in_buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, in_buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(in_data), gl.STATIC_DRAW);

    out_add_buffer = gl.createBuffer();
    gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, out_add_buffer);
    gl.bufferData(gl.TRANSFORM_FEEDBACK_BUFFER, Float32Array.BYTES_PER_ELEMENT * in_data.length, gl.STATIC_DRAW);

    // Create the transform feedback shader
    program = wtu.setupTransformFeedbackProgram(gl, ["vshader", "fshader"],
        ["out_add"], gl.SEPARATE_ATTRIBS,
        ["in_data"]);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "linking transform feedback shader should not set an error");
    shouldBeNonNull("program");

    // Draw the the transform feedback buffers
    tf = gl.createTransformFeedback();

    gl.enableVertexAttribArray(0);
    gl.bindBuffer(gl.ARRAY_BUFFER, in_buffer);
    gl.vertexAttribPointer(0, 4, gl.FLOAT, false, 16, 0);

    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
    gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, out_add_buffer);

    gl.enable(gl.RASTERIZER_DISCARD);
    gl.beginTransformFeedback(gl.POINTS);

    debug("Testing switching program while transform feedback is active");
    gl.pauseTransformFeedback();
    var program2 = wtu.setupSimpleColorProgram(gl);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "Switching program while transform feedback is active and paused should succeed");
    gl.useProgram(program);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "Switching program while transform feedback is active and paused should succeed");
    gl.resumeTransformFeedback();
    gl.useProgram(program2);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Switching program while transform feedback is active should fail");
    shouldBe("gl.getParameter(gl.CURRENT_PROGRAM)", "program");

    debug("Testing deleting an active transform feedback object");
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors before testing deletion");
    gl.deleteTransformFeedback(tf);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Deleting the transform feedback while active should fail, and have no effect");
    shouldBe("gl.isTransformFeedback(tf)", "true");
    debug("Resuming testing of single-output transform feedback");

    gl.drawArrays(gl.POINTS, 0, 3);

    gl.endTransformFeedback();
    gl.disable(gl.RASTERIZER_DISCARD);

    gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);

    // Verify the output buffer contents
    var add_expected = [
        3.0, 5.0, 7.0, 9.0,
        4.0, 7.0, 12.0, 21.0,
        2.75, 3.5, 4.25, 5.0
    ];
    gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, out_add_buffer);
    wtu.checkFloatBuffer(gl, gl.TRANSFORM_FEEDBACK_BUFFER, add_expected);

    gl.deleteBuffer(in_buffer);
    gl.deleteBuffer(out_add_buffer);
    gl.deleteProgram(program);
    gl.deleteTransformFeedback(tf);

    tf = null;
    program = null;
}

function runTwoOutputFeedbackTest() {
    debug("");
    debug("Testing two-output transform feedback");

    // Build the input and output buffers
    var in_data = [
        1.0, 2.0, 3.0, 4.0,
        2.0, 4.0, 8.0, 16.0,
        0.75, 0.5, 0.25, 0.0
    ];

    var in_buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, in_buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(in_data), gl.STATIC_DRAW);

    out_add_buffer = gl.createBuffer();
    gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, out_add_buffer);
    gl.bufferData(gl.TRANSFORM_FEEDBACK_BUFFER, Float32Array.BYTES_PER_ELEMENT * in_data.length, gl.STATIC_DRAW);

    var out_mul_buffer = gl.createBuffer();
    gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, out_mul_buffer);
    gl.bufferData(gl.TRANSFORM_FEEDBACK_BUFFER, Float32Array.BYTES_PER_ELEMENT * in_data.length, gl.STATIC_DRAW);

    // Create the transform feedback shader
    program = wtu.setupTransformFeedbackProgram(gl, ["vshader", "fshader"],
        ["out_add", "out_mul"], gl.SEPARATE_ATTRIBS,
        ["in_data"]);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "linking transform feedback shader should not set an error");
    shouldBeNonNull("program");

    // Create a query object to check the number of primitives written
    query = gl.createQuery();

    // Draw the the transform feedback buffers
    tf = gl.createTransformFeedback();

    gl.enableVertexAttribArray(0);
    gl.bindBuffer(gl.ARRAY_BUFFER, in_buffer);
    gl.vertexAttribPointer(0, 4, gl.FLOAT, false, 16, 0);

    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
    gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, out_add_buffer);
    gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, out_mul_buffer);

    gl.enable(gl.RASTERIZER_DISCARD);
    gl.beginTransformFeedback(gl.POINTS);
    gl.beginQuery(gl.TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, query);

    gl.drawArrays(gl.POINTS, 0, 3);

    gl.endQuery(gl.TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
    gl.endTransformFeedback();
    gl.disable(gl.RASTERIZER_DISCARD);

    gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);
    gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, null);

    // Verify the output buffer contents
    var add_expected = [
        3.0, 5.0, 7.0, 9.0,
        4.0, 7.0, 12.0, 21.0,
        2.75, 3.5, 4.25, 5.0
    ];
    gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, out_add_buffer);
    wtu.checkFloatBuffer(gl, gl.TRANSFORM_FEEDBACK_BUFFER, add_expected);

    var mul_expected = [
        2.0, 6.0, 12.0, 20.0,
        4.0, 12.0, 32.0, 80.0,
        1.5, 1.5, 1.0, 0.0
    ];
    gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, out_mul_buffer);
    wtu.checkFloatBuffer(gl, gl.TRANSFORM_FEEDBACK_BUFFER, mul_expected);

    gl.deleteBuffer(in_buffer);
    gl.deleteBuffer(out_add_buffer);
    gl.deleteBuffer(out_mul_buffer);
    gl.deleteProgram(program);
    gl.deleteTransformFeedback(tf);

    tf = null;
    program = null;

    // Check the result of the query. It should not be available yet.
    // This constant was chosen arbitrarily to take around 1 second on
    // one WebGL implementation on one desktop operating system. (Busy-
    // loops based on calling Date.now() have been found unreliable.)
    var numEarlyTests = 50000;
    while (--numEarlyTests > 0) {
        gl.finish();
        if (gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE)) {
            testFailed("Query's result became available too early");
            finishTest();
            return;
        }
    }
    testPassed("TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN query's result didn't become available too early");

    // Complete the rest of the test asynchronously.
    requestAnimationFrame(completeTransformFeedbackQueryTest);
}

function runUnboundDeleteTest() {
    debug("");
    debug("Testing deleting buffers attached to an unbound transform feedback object");

    // Theoretically it would be possible to verify the result of performing
    // transform feedback into a deleted buffer object. The buffer would have to
    // be latched into a VAO as well as into the transform feedback object. In
    // order to get the results out of the output buffer, it would be necessary
    // to run transform feedback again, reading from the buffer bound to the
    // VAO, and writing into a (non-deleted) buffer object latched into the
    // transform feedback object. It's not possible to arrange things to be able
    // to copyBufferSubData from the deleted buffer object into a temporary one
    // for readback.

    // This would be a lot of code to test an unlikely corner case, so instead,
    // this test verifies simpler behaviors.

    out_add_buffer = gl.createBuffer();
    const output_buffer_length = Float32Array.BYTES_PER_ELEMENT * 16;
    gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, out_add_buffer);
    gl.bufferData(gl.TRANSFORM_FEEDBACK_BUFFER, output_buffer_length, gl.STATIC_DRAW);

    // Set up the transform feedback object
    tf = gl.createTransformFeedback();
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
    gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, out_add_buffer);

    // Unbind transform feedback and delete out_add_buffer.
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
    gl.deleteBuffer(out_add_buffer);
    debug("isBuffer should report false after deletion");
    shouldBe("gl.isBuffer(out_add_buffer)", "false");

    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
    debug("Transform feedback object should keep output buffer alive");
    shouldBe("gl.getIndexedParameter(gl.TRANSFORM_FEEDBACK_BUFFER_BINDING, 0)", "out_add_buffer");
    // Deleting the buffer again while the transform feedback is bound shouldn't unbind it.
    gl.deleteBuffer(out_add_buffer);
    debug("Deleting output buffer again should be a no-op");
    shouldBe("gl.getIndexedParameter(gl.TRANSFORM_FEEDBACK_BUFFER_BINDING, 0)", "out_add_buffer");

    // Try unbinding and rebinding the transform feedback object just
    // to make sure that has no effect on the attached output buffer.
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
    debug("Transform feedback object should still keep output buffer alive");
    shouldBe("gl.getIndexedParameter(gl.TRANSFORM_FEEDBACK_BUFFER_BINDING, 0)", "out_add_buffer");

    gl.deleteTransformFeedback(tf);

    tf = null;
    out_add_buffer = null;
}

function runBoundDeleteTest() {
    debug("");
    debug("Testing deleting buffers attached to a bound transform feedback object");

    out_add_buffer = gl.createBuffer();
    const output_buffer_length = Float32Array.BYTES_PER_ELEMENT * 16;
    gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, out_add_buffer);
    gl.bufferData(gl.TRANSFORM_FEEDBACK_BUFFER, output_buffer_length, gl.STATIC_DRAW);

    // Set up the transform feedback object
    tf = gl.createTransformFeedback();
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
    gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, out_add_buffer);

    // Delete the output buffer
    gl.deleteBuffer(out_add_buffer);

    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
    debug("Buffer should have been unbound from active transform feedback");
    shouldBeNull("gl.getIndexedParameter(gl.TRANSFORM_FEEDBACK_BUFFER_BINDING, 0)");

    gl.deleteTransformFeedback(tf);

    tf = null;
    out_add_buffer = null;
}

var retArray;

function verifyGetBufferSubData(expected) {
    wtu.shouldGenerateGLError(gl, expected, "gl.getBufferSubData(gl.TRANSFORM_FEEDBACK_BUFFER, 0, retArray, 0, retArray.length)");
}

function runGetBufferSubDataTest() {
    debug("");
    debug("Test that getBufferSubData...");

    // Build the input and output buffers
    var in_data = [
        1.0, 2.0, 3.0, 4.0,
        2.0, 4.0, 8.0, 16.0,
        0.75, 0.5, 0.25, 0.0
    ];

    retArray = new Float32Array(in_data.length);

    var in_buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, in_buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(in_data), gl.STATIC_DRAW);

    out_add_buffer = gl.createBuffer();
    gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, out_add_buffer);
    gl.bufferData(gl.TRANSFORM_FEEDBACK_BUFFER, Float32Array.BYTES_PER_ELEMENT * in_data.length, gl.STATIC_DRAW);

    // Create the transform feedback shader
    program = wtu.setupTransformFeedbackProgram(gl, ["vshader", "fshader"],
        ["out_add"], gl.SEPARATE_ATTRIBS,
        ["in_data"]);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "linking transform feedback shader should not set an error");
    shouldBeNonNull("program");

    // Draw the the transform feedback buffers
    tf = gl.createTransformFeedback();

    gl.enableVertexAttribArray(0);
    gl.bindBuffer(gl.ARRAY_BUFFER, in_buffer);
    gl.vertexAttribPointer(0, 4, gl.FLOAT, false, 16, 0);

    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);

    debug("... passes when a transform feedback object is not bound");
    verifyGetBufferSubData(gl.NO_ERROR);

    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
    gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, out_add_buffer);

    debug("... passes when a transform feedback object is bound but not active");
    verifyGetBufferSubData(gl.NO_ERROR);

    gl.enable(gl.RASTERIZER_DISCARD);
    gl.beginTransformFeedback(gl.POINTS);

    debug("... fails when a transform feedback object is active");
    verifyGetBufferSubData(gl.INVALID_OPERATION);

    gl.drawArrays(gl.POINTS, 0, 3);

    gl.endTransformFeedback();
    gl.disable(gl.RASTERIZER_DISCARD);

    gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);

    // Verify the output buffer contents
    var add_expected = [
        3.0, 5.0, 7.0, 9.0,
        4.0, 7.0, 12.0, 21.0,
        2.75, 3.5, 4.25, 5.0
    ];
    gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, out_add_buffer);
    wtu.checkFloatBuffer(gl, gl.TRANSFORM_FEEDBACK_BUFFER, add_expected);

    tf = null;
    program = null;
}

function completeTransformFeedbackQueryTest() {
    debug("");
    debug("Testing TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN query");

    ++numberOfQueryCompletionAttempts;
    if (numberOfQueryCompletionAttempts > 500) {
        testFailed("Query didn't become available in a reasonable time");
        finishTest();
        return;
    }

    if (!gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE)) {
        requestAnimationFrame(completeTransformFeedbackQueryTest);
        return;
    }

    var result = gl.getQueryParameter(query, gl.QUERY_RESULT);
    if (result == 3) {
        testPassed("TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN query returned a correct result (3)");
    } else {
        testFailed("TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN query returned an incorrect result " + result + " (expected 3)");
    }

    runVaryingsTest();
}

function verifyTransformFeedbackVarying(prog, index, valid, name) {
    activeInfo = gl.getTransformFeedbackVarying(prog, index);
    if (valid) {
        wtu.glErrorShouldBe(gl, gl.NO_ERROR,
            "Should be no errors from valid getTransformFeedbackVarying.");
        shouldBeNonNull("activeInfo");
        shouldBe("activeInfo.name", "'" + name + "'");
        shouldBe("activeInfo.type", "gl.FLOAT_VEC4");
        shouldBe("activeInfo.size", "1");
    } else {
        wtu.glErrorShouldBe(gl, gl.INVALID_VALUE,
            "Should be INVALID_VALUE when calling getTransformFeedbackVarying with an invalid index.");
        shouldBeNull("activeInfo");
    }
}

function runVaryingsTest() {
    debug("");
    debug("Testing transform feedback varyings");

    // Create the transform feedback shader. This is explicitly run after runTwoOutputFeedbackTest,
    // as re-linking the shader here will test browser caching.
    program = wtu.setupTransformFeedbackProgram(gl, ["vshader", "fshader"],
        ["out_add", "out_mul"], gl.SEPARATE_ATTRIBS,
        ["in_data"]);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "linking transform feedback shader should not set an error");
    shouldBeNonNull("program");

    // Check the varyings
    shouldBe("gl.getProgramParameter(program, gl.TRANSFORM_FEEDBACK_VARYINGS)", "2");
    verifyTransformFeedbackVarying(program, 0, true, "out_add");
    verifyTransformFeedbackVarying(program, 1, true, "out_mul");
    verifyTransformFeedbackVarying(program, 2, false);

    // transformFeedbackVaryings() doesn't take effect until a successful link.
    gl.transformFeedbackVaryings(program, ["out_mul"], gl.SEPARATE_ATTRIBS);
    shouldBe("gl.getProgramParameter(program, gl.TRANSFORM_FEEDBACK_VARYINGS)", "2");
    verifyTransformFeedbackVarying(program, 0, true, "out_add");
    verifyTransformFeedbackVarying(program, 1, true, "out_mul");
    verifyTransformFeedbackVarying(program, 2, false);

    // Now relink.
    gl.linkProgram(program);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "linking transform feedback shader should not set an error");
    shouldBeTrue("gl.getProgramParameter(program, gl.LINK_STATUS)");
    shouldBe("gl.getProgramParameter(program, gl.TRANSFORM_FEEDBACK_VARYINGS)", "1");
    verifyTransformFeedbackVarying(program, 0, true, "out_mul");
    verifyTransformFeedbackVarying(program, 1, false);
    verifyTransformFeedbackVarying(program, 2, false);

    // Test recompiling/relinking the program
    // Regression test for http://crbug.com/716018
    var skipCompileStatus = true;
    program = wtu.setupTransformFeedbackProgram(gl, ["vshader", "fshader"],
        ["out_add", "out_mul"], gl.SEPARATE_ATTRIBS,
        ["in_data"], undefined, undefined, skipCompileStatus);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "linking transform feedback shader should not set an error");
    shouldBeTrue("gl.getProgramParameter(program, gl.LINK_STATUS)");
    shouldBe("gl.getProgramParameter(program, gl.TRANSFORM_FEEDBACK_VARYINGS)", "2");
    verifyTransformFeedbackVarying(program, 0, true, "out_add");
    verifyTransformFeedbackVarying(program, 1, true, "out_mul");
    verifyTransformFeedbackVarying(program, 2, false);

    runContextLostOneOutputFeedbackTest();
}

function runContextLostOneOutputFeedbackTest() {
    var ext = gl.getExtension("WEBGL_lose_context");
    if (!ext) {
        debug("No WEBGL_lose_context support");
        finishTest();
        return;
    }
    debug("");
    debug("Testing switching program after context lost while transform feedback is active");

    var in_buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, in_buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(12), gl.STATIC_DRAW);

    out_add_buffer = gl.createBuffer();
    gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, out_add_buffer);
    gl.bufferData(gl.TRANSFORM_FEEDBACK_BUFFER, Float32Array.BYTES_PER_ELEMENT * 12, gl.STATIC_DRAW);

    // Create an extra program to try switching to
    var program2 = wtu.setupSimpleColorProgram(gl);

    // Create the transform feedback shader
    program = wtu.setupTransformFeedbackProgram(gl, ["vshader", "fshader"],
        ["out_add"], gl.SEPARATE_ATTRIBS,
        ["in_data"]);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "linking transform feedback shader should not set an error");
    shouldBeNonNull("program");

    // Draw the the transform feedback buffers
    tf = gl.createTransformFeedback();

    gl.enableVertexAttribArray(0);
    gl.bindBuffer(gl.ARRAY_BUFFER, in_buffer);
    gl.vertexAttribPointer(0, 4, gl.FLOAT, false, 16, 0);

    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
    gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, out_add_buffer);

    gl.beginTransformFeedback(gl.POINTS);

    debug("Calling loseContext()");
    ext.loseContext();
    shouldBeTrue("gl.isContextLost()");
    shouldBe("gl.getError()", "gl.CONTEXT_LOST_WEBGL");
    shouldBe("gl.getError()", "gl.NO_ERROR");
    debug("Trying to switch program");
    gl.useProgram(program2);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "No transform feedback error generated on lost context");
    finishTest();
}

function runUnchangedBufferBindingsTest() {
    debug("");
    debug("Testing that buffer bindings cannot be changed while transform feedback is active");

    program = wtu.setupTransformFeedbackProgram(
        gl, [wtu.simpleVertexShader, wtu.simpleColorFragmentShader], ['gl_Position'], gl.INTERLEAVED_ATTRIBS);

    tf = gl.createTransformFeedback();
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);

    buf = gl.createBuffer();
    gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buf);

    gl.beginTransformFeedback(gl.POINTS);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "Transform feedback is active");

    gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, gl.createBuffer());
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Cannot change the bound buffer while transform feedback is active");
    shouldBe("gl.getIndexedParameter(gl.TRANSFORM_FEEDBACK_BUFFER_BINDING, 0)", "buf");

    gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buf);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Cannot rebind the same buffer while transform feedback is active");

    gl.bindBufferRange(gl.TRANSFORM_FEEDBACK_BUFFER, 0, gl.createBuffer(), 0, 64);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Cannot change the bound buffer while transform feedback is active");
    shouldBe("gl.getIndexedParameter(gl.TRANSFORM_FEEDBACK_BUFFER_BINDING, 0)", "buf");

    gl.bindBufferRange(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buf, 0, 64);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Cannot rebind the same buffer while transform feedback is active");

    gl.endTransformFeedback();
    gl.deleteTransformFeedback(tf);
    gl.deleteBuffer(buf);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "No extra errors after the test");
}

function runNoOutputsTest() {
    debug("");
    debug("Testing transform feedback with no varyings to record");

    tf = gl.createTransformFeedback();
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);

    buf = gl.createBuffer();
    gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buf);

    for (const mode of ['SEPARATE_ATTRIBS', 'INTERLEAVED_ATTRIBS']) {
        program = wtu.setupTransformFeedbackProgram(
            gl, [wtu.simpleVertexShader, wtu.simpleColorFragmentShader], [], gl[mode]);

        debug(`Testing with ${mode}`);
        gl.beginTransformFeedback(gl.POINTS);
        wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "beginTransformFeedback: No varyings to record");
        gl.useProgram(null);
        wtu.glErrorShouldBe(gl, gl.NO_ERROR, "useProgram: Transform feedback is not active");
        gl.endTransformFeedback();
        wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "endTransformFeedback: Transform feedback is not active");
    }

    gl.deleteTransformFeedback(tf);
    gl.deleteBuffer(buf);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "No extra errors after the test");
}

debug("");
</script>

</body>
</html>
